package com.tools.web.work.obs;

import com.obs.services.ObsClient;
import com.obs.services.exception.ObsException;
import com.obs.services.model.*;
import com.tools.common.container.OwnPairs;
import com.tools.common.container.RowKit;
import com.tools.common.exception.IORuntimeException;
import com.tools.common.exception.InvalidOperationException;
import com.tools.common.io.IOKit;
import com.tools.common.io.ProfileKit;
import com.tools.common.object.Note;
import com.tools.common.object.ObjKit;
import com.tools.common.object.string.StrKit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * OBS 对象存储服务的工具类
 * 用于操作文件上传、下载、存储等等服务
 * */
@Note("OBS 对象存储服务的工具类" +
        "用于操作文件上传、下载、存储等等服务")
public final class OBSKit {

    private OBSKit() {}

    @Note("工具类默认的 OBS 客户端实例。采用懒加载的方式实例化")
    private static volatile ObsClient CLIENT;

    @Note("日志输出对象")
    private static final Logger LOGGER = LoggerFactory.getLogger(OBSKit.class);

    @Note("默认超过 100 MB就可以开启大文件分段上传了")
    private static volatile long FILE_SIZE_CRITICAL = 100 * 1024 * 1024;


    /* **************************************************************************************
     *
     *          其他方法
     *
     * **************************************************************************************
     * */

    public static long getFileSizeCritical() {
        return FILE_SIZE_CRITICAL;
    }

    @Note("设置大文件分段上传的阈值，单位为字节。但是必须要大于 100 MB")
    public static void setFileSizeCritical(long fileSizeCritical) {
        synchronized (OBSKit.class) {
            if(fileSizeCritical < 100 * 1024 * 1024) return;
            FILE_SIZE_CRITICAL = fileSizeCritical;
        }
    }

    @Note("根据 OBSSource 对象的配置项参数构建一个 OBS 对象存储服务的客户端实例")
    public static ObsClient newObsClient(OBSSource source) {
        String accessKey = source.getAccessKey();
        String secretKey = source.getSecretKey();
        String endPoint = source.getEndPoint();
        if(StrKit.isAnyEmpty(accessKey, secretKey, endPoint)) {
            throw new IllegalArgumentException("构建 OBS 对象存储服务客户端实例必备的三个参数项: accessKey、secretKey、endPoint 都不能为空");
        }
        return new ObsClient(accessKey, secretKey, endPoint);
    }


    @Note("使用本工具类默认的客户端实例对字节输入流的全部内容进行 MD5 值编码")
    public static String base64MD5(InputStream inputStream) {
        private_lazyLoadOBSClient();
        return base64MD5(CLIENT, inputStream);
    }


    @Note("使用外部传入的客户端实例对字节输入流的全部内容进行 MD5 值编码")
    public static String base64MD5(ObsClient client, InputStream inputStream) {
        try {
            return client.base64Md5(inputStream);
        } catch (NoSuchAlgorithmException | IOException e) {
            throw new IORuntimeException(e);
        }
    }


    @Note("使用本工具类默认的客户端实例对字节输入流从 offset 开始到 length 结束的内容进行 MD5 值编码")
    public static String base64MD5(InputStream inputStream, long length, long offset) {
        private_lazyLoadOBSClient();
        return base64MD5(CLIENT, inputStream, length, offset);
    }


    @Note("使用外部传入的客户端实例对字节输入流从 offset 开始到 length 结束的内容进行 MD5 值编码")
    public static String base64MD5(ObsClient client, InputStream inputStream, long length, long offset) {
        try {
            return client.base64Md5(inputStream, length, offset);
        } catch (NoSuchAlgorithmException | IOException e) {
            throw new IORuntimeException(e);
        }
    }


    @Note("新建分段上传任务，获取分段上传任务唯一的标识 ID")
    public static String newSegmentUploadTask(ObsClient client, OBSBuilder builder) {
        private_clientNotNull(client);
        private_obsBuilderIsNotNull(builder);
        File bigFile = builder.getBigFile();
        if(bigFile == null) throw new NullPointerException("需要上传的大文件为 null，构建分段上传任务失败");
        long fileSize = bigFile.length();
        if(fileSize < FILE_SIZE_CRITICAL) throw new InvalidOperationException("需要上传的大文件的实际大小为 【" + fileSize + "】 字节" +
                "没有达到指定的阈值 【" + FILE_SIZE_CRITICAL + "】 字节，无需使用分段上传");
        try {
            InitiateMultipartUploadRequest request =
                    new InitiateMultipartUploadRequest(builder.getBucketName(), builder.getObjectKey());
            ObjectMetadata metadata = private_additionalHandleAndGetMetaData(builder);
            request.setMetadata(metadata);
            InitiateMultipartUploadResult result = client.initiateMultipartUpload(request);
            String uploadId = result.getUploadId();
            LOGGER.info("分段上传任务创建成功，分段 ID 为: {}", uploadId);
            return uploadId;
        } catch (Exception e) {
            e.printStackTrace();
            throw new IORuntimeException("构建分段上传任务失败，请检查并消除异常再重新调用");
        }
    }


    @Note("取消分段上传的所有段")
    public static HeaderResponse abortSegmentUpload(String bucketName, String objectKey, String uploadID) {
        private_stringIsNotNull(bucketName, "桶的名称不能为空");
        private_stringIsNotNull(objectKey, "文件对象的名称不能为空");
        private_stringIsNotNull(uploadID, "分段上传的唯一标识 ID 不能为空");
        AbortMultipartUploadRequest request = new AbortMultipartUploadRequest(bucketName, objectKey, uploadID);
        return CLIENT.abortMultipartUpload(request);
    }

    /* **************************************************************************************
     *
     *          上传
     *
     * **************************************************************************************
     * */

    @Note("使用本工具类默认的 OBS 客户端创建文件夹")
    public static PutObjectResult createDir(String bucketName, String dirName) {
        private_lazyLoadOBSClient();
        return createDir(CLIENT, bucketName, dirName);
    }

    @Note("使用指定的 OBS 客户端创建文件夹")
    public static PutObjectResult createDir(ObsClient client, String bucketName, String dirName) {
        private_stringIsNotNull(bucketName, "桶的名称不能为空");
        private_stringIsNotNull(dirName, "文件夹的名称不能为空");
        if(dirName.charAt(dirName.length() - 1) != '/')  {
            dirName += '/';
        }
        return client.putObject(bucketName, dirName, new ByteArrayInputStream(new byte[0]));
    }

    @Note("使用本工具类默认的客户端上传单个文件")
    public static PutObjectResult upload(OBSBuilder builder) {
        private_lazyLoadOBSClient();
        return upload(CLIENT, builder);
    }

    @Note("使用外部传入的客户端上传单个文件")
    public static PutObjectResult upload(ObsClient client, OBSBuilder builder) {
        private_obsBuilderIsNotNull(builder);
        InputStream fileStream = builder.getFileStream();
        if(fileStream == null) throw new NullPointerException("要上传的文件对象的字节输入流不能为 null");
        ObjectMetadata metadata = private_additionalHandleAndGetMetaData(builder);
        //是否开启 MD5 校验
        if(builder.isUseMD5()) {
            String md5 = base64MD5(client, fileStream);
            metadata.setContentMd5(md5);
        }
        //是否开启进度监测
        if(builder.isEnableProgress()) {
            ProgressListener listener = builder.getProgressListener();
            PutObjectRequest request = new PutObjectRequest();
            request.setInput(fileStream);
            request.setProgressListener(listener);
            request.setProgressInterval(builder.getProgressInterval());
            return client.putObject(request);
        }
        return client.putObject(builder.getBucketName(),
                builder.getObjectKey(),
                fileStream,
                metadata);
    }

    @Note("使用本工具类默认的客户端上传多个文件")
    public static List<PutObjectResult> uploads(OBSBuilder... builders) {
        return uploads(CLIENT, builders);
    }

    @Note("使用外部传入的客户端上传多个文件")
    public static List<PutObjectResult> uploads(ObsClient client, OBSBuilder... builders) {
        if(builders == null || builders.length == 0) throw new IllegalArgumentException("OBS 构建对象数组为空，没有可用于上传的文件");
        List<PutObjectResult> results = new ArrayList<>(builders.length);
        for(OBSBuilder builder : builders) {
            PutObjectResult result = upload(client, builder);
            results.add(result);
        }
        return results;
    }


    @Note("使用本工具类默认的客户端上传多个文件")
    public static List<PutObjectResult> uploads(Iterable<OBSBuilder> builders) {
        return uploads(CLIENT, builders);
    }

    @Note("使用外部传入的客户端上传多个文件")
    public static List<PutObjectResult> uploads(ObsClient client, Iterable<OBSBuilder> builders) {
        if(builders == null) throw new IllegalArgumentException("OBS 构建对象可迭代容器为 null，没有可用于上传的文件");
        int size = RowKit.getIterableSize(builders);
        if(size == 0) throw new IllegalArgumentException("OBS 构建对象可迭代容器为空，没有可用于上传的文件");
        List<PutObjectResult> results = new ArrayList<>(size);
        for(OBSBuilder builder : builders) {
            results.add(upload(client, builder));
        }
        return results;
    }

    @Note("使用本工具类默认的 OBS 客户端实例在指定的位置追加上传文件，如果是第一次追加，位置为 0。" +
            "否则可以通过上次的 AppendObjectResult 实例的 getNextPosition 方法获取下一次开始追加的位置")
    public static AppendObjectResult appendUpload(OBSBuilder builder, int appendPosition) {
        private_lazyLoadOBSClient();
        return appendUpload(CLIENT, builder, appendPosition);
    }

    @Note("使用指定的 OBS 客户端实例在指定的位置追加上传文件，如果是第一次追加，位置为 0。" +
            "否则可以通过上次的 AppendObjectResult 实例的 getNextPosition 方法获取下一次开始追加的位置")
    public static AppendObjectResult appendUpload(ObsClient client, OBSBuilder builder, int appendPosition) {
        private_clientNotNull(client);
        private_obsBuilderIsNotNull(builder);
        AppendObjectRequest request = new AppendObjectRequest(builder.getBucketName());
        request.setObjectKey(builder.getObjectKey());
        ObjectMetadata metadata = private_additionalHandleAndGetMetaData(builder);
        if(appendPosition < 0) appendPosition = 0;
        request.setPosition(appendPosition);
        InputStream fileStream = builder.getFileStream();
        if(fileStream == null) throw new NullPointerException("要追加上传的文件对象的字节输入流不能为 null");
        request.setInput(fileStream);
        //是否开启 MD5 校验
        if(builder.isUseMD5()) {
            String s = base64MD5(client, fileStream);
            metadata.setContentMd5(s);
        }
        request.setMetadata(metadata);
        if(builder.isEnableProgress()) {
            request.setProgressListener(builder.getProgressListener());
            request.setProgressInterval(builder.getProgressInterval());
        }
        AppendObjectResult result = client.appendObject(request);
        LOGGER.info("追加对象成功，当前追加位置为 【{}】，下次追加的位置为 【{}】，可通过 AppendObjectResult.getNextPosition() 获取",
                appendPosition,
                result.getNextPosition());
        return result;
    }

    @Note("使用本工具类默认的 OBS 客户端实现分段上传 + 合并大文件的操作。上传过程中出错不会续传，会清除所有已上传的段。")
    public static CompleteMultipartUploadResult segmentUploadNotContinue(OBSBuilder builder) {
        private_lazyLoadOBSClient();
        return segmentUploadNotContinue(CLIENT, builder);
    }

    @Note("使用指定的 OBS 客户端实现分段上传 + 合并大文件的操作。上传过程中出错不会续传，会清除所有已上传的段。")
    public static CompleteMultipartUploadResult segmentUploadNotContinue(ObsClient client, OBSBuilder builder) {
        String uploadID = newSegmentUploadTask(client, builder);
        String bucketName = builder.getBucketName();
        try {
            File bigFile = builder.getBigFile();
            long fileSize = bigFile.length();
            //每一段大概多少个字节大小
            long partSize = builder.getPartSize();
            //整个文件分为多少段
            int partCount = (int) ((fileSize % partSize == 0) ? (fileSize / partSize) : (fileSize / partSize) + 1);
            LOGGER.info("分段上传任务开始，文件大小: 【{}】 字节，每段大小 【{}】 字节，共分 {} 段上传", fileSize, partSize, partCount);
            //分段集合
            List<PartEtag> partEtagList = new ArrayList<>(partCount);
            UploadPartRequest request;
            boolean useMD5 = builder.isUseMD5();
            InputStream fileStream = IOKit.fileInputStream(bigFile);
            String objectKey = builder.getObjectKey();
            request = new UploadPartRequest(bucketName, objectKey);
            //进度展示
            if(builder.isEnableProgress()) {
                request.setProgressListener(builder.getProgressListener());
                request.setProgressInterval(builder.getProgressInterval());
            }
            // 设置Upload ID
            request.setUploadId(uploadID);
            for (int i = 0; i < partCount; i++) {
                // 分段号
                int partNumber = i + 1;
                // 分段在文件中的起始位置
                long offset = i * partSize;
                // 设置分段号，范围是1~10000，
                request.setPartNumber(partNumber);
                // 设置将要上传的大文件
                if(useMD5) {
                    String md5 = base64MD5(fileStream, partSize, offset);
                    request.setContentMd5(md5);
                }
                request.setInput(fileStream);
                // 设置段偏移量
                request.setOffset(offset);
                //上传
                UploadPartResult part = client.uploadPart(request);
                partEtagList.add(new PartEtag(part.getEtag(), part.getPartNumber()));
            }
            LOGGER.info("分段上传完成，正在开始合并分段...");
            //合并所有分段为一个完整的大文件
            CompleteMultipartUploadRequest comRequest =
                    new CompleteMultipartUploadRequest(bucketName, objectKey, uploadID, partEtagList);
            CompleteMultipartUploadResult result = client.completeMultipartUpload(comRequest);
            LOGGER.info("分段上传完成!");
            return result;
        } catch (ObsException e) {
            String objectKey = builder.getObjectKey();
            LOGGER.error("分段上传过程出错，任务终止，正在删除已上传完成的分段...\n" +
                            "具体信息：文件名称 {}，HTTP 状态码:{}，错误码:{}，Request ID:{}",
                    objectKey, e.getResponseCode(), e.getErrorCode(), e.getErrorRequestId());
            //取消分段上传
            HeaderResponse response = abortSegmentUpload(bucketName, objectKey, uploadID);
            LOGGER.info("取消请求的响应结果：{}", response);
            throw e;
        }
    }


    @Note("使用指定的 OBS 客户端实例执行大文件分段上传 + 断点续传功能")
    public static CompleteMultipartUploadResult segmentUploadIsContinue(OBSBuilder builder) {
        private_lazyLoadOBSClient();
        return segmentUploadIsContinue(CLIENT, builder);
    }

    @Note("使用指定的 OBS 客户端实例执行大文件分段上传 + 断点续传功能")
    public static CompleteMultipartUploadResult segmentUploadIsContinue(ObsClient client, OBSBuilder builder) {
        private_clientNotNull(client);
        private_obsBuilderIsNotNull(builder);
        File bigFile = builder.getBigFile();
        if(bigFile == null) throw new NullPointerException("待上传的大文件为 null");
        UploadFileRequest request = new UploadFileRequest(builder.getBucketName(), builder.getObjectKey());
        request.setUploadFile(bigFile.getAbsolutePath());
        request.setPartSize(builder.getPartSize());
        request.setTaskNum(builder.getTaskNumber());
        ObjectMetadata metadata = private_additionalHandleAndGetMetaData(builder);
        request.setObjectMetadata(metadata);
        if(builder.isEnableProgress()) {
            request.setProgressListener(builder.getProgressListener());
            request.setProgressInterval(builder.getProgressInterval());
        }
        //开启断点续传
        request.setEnableCheckpoint(true);
        return private_continueRequestUpload(client, request, builder.getTryCount());
    }


    @Note("使用本工具类默认的 OBS 客户端实例列举分段上传中已上传的段。其中 pageSize 为负数或者大于 1000 时列举所有已上传的段。offset 为每次列举的起始位置偏移量")
    public static List<ListPartsResult> listSegments(OBSBuilder builder, String uploadID, int offset, int pageSize) {
        private_lazyLoadOBSClient();
        return listSegments(CLIENT, builder, uploadID, offset, pageSize);
    }

    @Note("使用指定的 OBS 客户端实例列举分段上传中已上传的段。其中 pageSize 为负数或者大于 1000 时列举所有已上传的段。offset 为每次列举的起始位置偏移量")
    public static List<ListPartsResult> listSegments(ObsClient client, OBSBuilder builder, String uploadID, int offset, int pageSize) {
        private_clientNotNull(client);
        String bucketName = builder.getBucketName();
        String objectKey = builder.getObjectKey();
        private_stringIsNotNull(uploadID, "分段上传的唯一标识 ID 不能为空");
        if(offset < 0) throw new IllegalArgumentException("起始页码必须大于等于 0");
        if(pageSize == 0) return new ArrayList<>();
        ListPartsRequest request = new ListPartsRequest(bucketName, objectKey, uploadID);
        ListPartsResult result;
        List<ListPartsResult> resultList = new ArrayList<>();
        //设置分页，如果 pageSize 大于 1000 或者为负数则返回全部
        request.setPartNumberMarker(offset);
        if(pageSize > 0 && pageSize <= 1000) {
            request.setMaxParts(pageSize);
            result = client.listParts(request);
            resultList.add(result);
            return resultList;
        }
        do {
            result = client.listParts(request);
            resultList.add(result);
            String numberMarker = result.getNextPartNumberMarker();
            offset = Integer.parseInt(numberMarker);
            request.setPartNumberMarker(offset);
        } while (result.isTruncated());
        return resultList;
    }

    @Note("使用本工具类默认的 OBS 客户端实例列举所有的正在分段上传的任务。如果要分页请使用逻辑分页。")
    public static List<MultipartUpload> listSegmentsUploadTask(String bucketName) {
        private_lazyLoadOBSClient();
        return listSegmentsUploadTask(CLIENT, bucketName);
    }

    @Note("使用指定的 OBS 客户端实例列举所有的正在分段上传的任务。如果要分页请使用逻辑分页。")
    public static List<MultipartUpload> listSegmentsUploadTask(ObsClient client, String bucketName) {
        private_clientNotNull(client);
        private_stringIsNotNull(bucketName, "桶的名称不能为空");
        ListMultipartUploadsRequest request = new ListMultipartUploadsRequest(bucketName);
        MultipartUploadListing result;
        List<MultipartUpload> resultList = new ArrayList<>();
        do {
            result = client.listMultipartUploads(request);
            resultList.addAll(result.getMultipartTaskList());
            request.setKeyMarker(result.getNextKeyMarker());
            request.setUploadIdMarker(result.getNextUploadIdMarker());
        } while (result.isTruncated());
        return resultList;
    }



    /* **************************************************************************************
     *
     *          其他操作
     *
     * **************************************************************************************
     * */



    @Note("使用指定的 OBS 客户端获取文件的元数据")
    public static ObjectMetadata getObjectMetaData(OBSBuilder builder) {
        private_lazyLoadOBSClient();
        return CLIENT.getObjectMetadata(builder.getBucketName(), builder.getObjectKey());
    }

    @Note("使用指定的 OBS 客户端获取文件的元数据")
    public static ObjectMetadata getObjectMetaData(ObsClient client, OBSBuilder builder) {
        return client.getObjectMetadata(builder.getBucketName(), builder.getObjectKey());
    }

    @Note("使用本工具类默认的 OBS 客户端获取文件对象")
    public static ObsObject getObject(OBSBuilder builder) {
        private_lazyLoadOBSClient();
        return getObject(CLIENT, builder);
    }

    @Note("使用指定的 OBS 客户端获取文件对象")
    public static ObsObject getObject(ObsClient client, OBSBuilder builder) {
        private_clientNotNull(client);
        private_obsBuilderIsNotNull(builder);
        GetObjectRequest request = new GetObjectRequest(builder.getBucketName(), builder.getObjectKey());
        if(builder.isEnableProgress()) {
            request.setProgressListener(builder.getProgressListener());
            request.setProgressInterval(builder.getProgressInterval());
        }
        return client.getObject(request);
    }


    @Note("使用本工具类默认的 OBS 客户端实例进行断点续传下载文件")
    public static DownloadFileResult segmentDownloadIsContinue(OBSBuilder builder, String downloadFilePath) {
        private_lazyLoadOBSClient();
        return segmentDownloadIsContinue(CLIENT, builder, downloadFilePath);
    }

    @Note("使用指定的 OBS 客户端实例进行断点续传下载文件")
    public static DownloadFileResult segmentDownloadIsContinue(ObsClient client, OBSBuilder builder, String downloadFilePath) {
        private_clientNotNull(client);
        private_obsBuilderIsNotNull(builder);
        if(downloadFilePath == null || downloadFilePath.isEmpty()) throw new IllegalArgumentException("下载文件到哪个路径？");
        DownloadFileRequest request = new DownloadFileRequest(builder.getBucketName(), builder.getObjectKey());
        request.setDownloadFile(downloadFilePath);
        request.setTaskNum(builder.getTaskNumber());
        request.setPartSize(builder.getPartSize());
        request.setEnableCheckpoint(true);
        return client.downloadFile(request);
    }

    @Note("使用本工具类默认的 OBS 客户端删除文件")
    public static InputStream download(String bucketName, String objectKey) {
        private_lazyLoadOBSClient();
        return download(CLIENT, bucketName, objectKey);
    }

    @Note("使用指定的 OBS 客户端下载指定的单个文件")
    public static InputStream download(ObsClient client, String bucketName, String objectKey) {
        ObsObject obsObject = client.getObject(bucketName, objectKey);
        return (obsObject == null) ? null : obsObject.getObjectContent();
    }

    @Note("使用本工具类默认的 OBS 客户端删除文件")
    public static DeleteObjectResult deleteFile(String bucketName, String objectKey) {
        private_lazyLoadOBSClient();
        return deleteFile(CLIENT, bucketName, objectKey);
    }

    @Note("使用指定的 OBS 客户端删除单个文件")
    public static DeleteObjectResult deleteFile(ObsClient client, String bucketName, String objectKey) {
        private_stringIsNotNull(bucketName, "桶的名称不能为空");
        private_stringIsNotNull(objectKey, "OBS 对象的 Key 不能为空");
        return client.deleteObject(bucketName, objectKey);
    }

    @Note("使用指定的 OBS 客户端删除多个文件")
    public static DeleteObjectsResult deleteFiles(String bucketName, String... keys) {
        private_lazyLoadOBSClient();
        return deleteFiles(CLIENT, bucketName, keys);
    }

    @Note("使用指定的 OBS 客户端删除多个文件")
    public static DeleteObjectsResult deleteFiles(ObsClient client, String bucketName, String... keys) {
        if(keys == null || keys.length == 0) return new DeleteObjectsResult();
        private_stringIsNotNull(bucketName, "桶的名称不能为空");
        DeleteObjectsRequest request = new DeleteObjectsRequest(bucketName);
        for (int i = 0; i < keys.length; i++) {
            String key = keys[i];
            private_stringIsNotNull(key, "第 " + i + " 个 Key 不能为空");
            request.addKeyAndVersion(key);
        }
        return client.deleteObjects(request);
    }

    @Note("使用指定的 OBS 客户端删除多个文件")
    public static DeleteObjectsResult deleteFiles(String bucketName, Iterable<String> keys) {
        private_lazyLoadOBSClient();
        return deleteFiles(CLIENT, bucketName, keys);
    }

    @Note("使用指定的 OBS 客户端删除多个文件")
    public static DeleteObjectsResult deleteFiles(ObsClient client, String bucketName, Iterable<String> keys) {
        private_stringIsNotNull(bucketName, "桶的名称不能为空");
        if(keys == null) return new DeleteObjectsResult();
        Iterator<String> iterator = keys.iterator();
        if(!iterator.hasNext()) return new DeleteObjectsResult();
        DeleteObjectsRequest request = new DeleteObjectsRequest(bucketName);
        int index = 0;
        while (iterator.hasNext()) {
            String key = iterator.next();
            private_stringIsNotNull(key, "第 " + index + " 个 Key 不能为空");
            request.addKeyAndVersion(key);
            index++;
        }
        return client.deleteObjects(request);
    }

    @Note("使用本工具类默认的 OBS 客户端判断指定的文件是否位于 OBS 服务器中")
    public static boolean fileExists(String bucketName, String objectKey) {
        private_lazyLoadOBSClient();
        return fileExists(CLIENT, bucketName, objectKey);
    }

    @Note("使用指定的 OBS 客户端判断指定的文件是否位于 OBS 服务器中")
    public static boolean fileExists(ObsClient client, String bucketName, String objectKey) {
        private_stringIsNotNull(bucketName, "桶的名称不能为空");
        private_stringIsNotNull(objectKey, "OBS 对象的 Key 不能为空");
        return client.doesObjectExist(bucketName, objectKey);
    }

    @Note("使用本工具类默认的 OBS 客户端设置指定文件对象元数据或者属性。" +
            "如果 headers 里的头信息需要在服务端以自定义元数据形式显示，则需要加上 x-obs-meta 前缀")
    public static ObjectMetadata setMetaData(OBSBuilder builder, Map headers) {
        private_lazyLoadOBSClient();
        return setMetaData(CLIENT, builder, headers);
    }

    @Note("使用指定的 OBS 客户端设置指定文件对象元数据或者属性。" +
            "如果 headers 里的头信息需要在服务端以自定义元数据形式显示，则需要加上 x-obs-meta 前缀")
    public static ObjectMetadata setMetaData(ObsClient client, OBSBuilder builder, Map headers) {
        return private_setMetaDataHandle(client, builder, headers);
    }

    @Note("使用本工具类默认的 OBS 客户端设置指定文件对象元数据或者属性。" +
            "如果 headers 里的头信息需要在服务端以自定义元数据形式显示，则需要加上 x-obs-meta 前缀")
    public static ObjectMetadata setMetaData(OBSBuilder builder, OwnPairs headers) {
        private_lazyLoadOBSClient();
        return setMetaData(CLIENT, builder, headers);
    }

    @Note("使用指定的 OBS 客户端设置指定文件对象元数据或者属性。" +
            "如果 headers 里的头信息需要在服务端以自定义元数据形式显示，则需要加上 x-obs-meta 前缀")
    public static ObjectMetadata setMetaData(ObsClient client, OBSBuilder builder, OwnPairs headers) {
        return private_setMetaDataHandle(client, builder, headers);
    }


    @Note("使用本工具类默认的 OBS 客户端分页列举文件对象。每一页最多 1000 条。" +
            "起始位置 offset 为 null 则从头开始列举，否则就是上一次的 ObjectListing.getMarker 作为下次起始位置。" +
            "前缀可以设置为以 / 结尾的文件夹的形式，表示分页列举该文件夹下所有的对象。" +
            "delimiter 可以作为多级文件夹的之间的层级分隔符")
    public static ObjectListing listObjects(String bucketName, String prefix, String delimiter, String offset, int pageSize) {
        private_lazyLoadOBSClient();
        return listObjects(CLIENT, bucketName, prefix, delimiter, offset, pageSize);
    }

    @Note("使用指定的 OBS 客户端分页列举文件对象。每一页最多 1000 条。" +
            "起始位置 offset 为 null 则从头开始列举，否则就是上一次的 ObjectListing.getMarker 作为下次起始位置。" +
            "前缀可以设置为以 / 结尾的文件夹的形式，表示分页列举该文件夹下所有的对象。" +
            "delimiter 可以作为多级文件夹的之间的层级分隔符")
    public static ObjectListing listObjects(ObsClient client, String bucketName, String prefix, String delimiter, String offset, int pageSize) {
        private_clientNotNull(client);
        private_stringIsNotNull(bucketName, "桶的名称不能为 null");
        ListObjectsRequest request = new ListObjectsRequest(bucketName);
        if(offset != null && !offset.isEmpty()) request.setMarker(offset);
        if(prefix != null && !prefix.isEmpty()) request.setPrefix(prefix);
        if(delimiter != null && !delimiter.isEmpty()) request.setDelimiter(delimiter);
        //如果一页条目数在 1000 以内，则直接请求返回
        if(pageSize >= 0 && pageSize <= 1000) {
            request.setMaxKeys(pageSize);
            return client.listObjects(request);
        }
        if(pageSize > 1000) {
            request.setMaxKeys(pageSize);
        }
        List<ObsObject> objects = new ArrayList<>(pageSize);
        List<ObsObject> commonPrefix = new ArrayList<>(pageSize);
        List<String> prefixList = new ArrayList<>(pageSize);
        ObjectListing res;
        do {
            res = client.listObjects(request);
            request.setMarker(res.getMarker());
            objects.addAll(res.getObjects());
            commonPrefix.addAll(res.getExtendCommonPrefixes());
            prefixList.addAll(res.getCommonPrefixes());
        } while (res.isTruncated());
        return new ObjectListing.Builder()
                .bucketName(bucketName)
                .truncated(res.isTruncated())
                .prefix(res.getPrefix())
                .marker(res.getMarker())
                .maxKeys(res.getMaxKeys())
                .delimiter(res.getDelimiter())
                .nextMarker(res.getNextMarker())
                .location(res.getLocation())
                .objectSummaries(objects)
                .extendCommonPrefixes(commonPrefix)
                .commonPrefixes(prefixList)
                .builder();
    }

    @Note("关闭本工具类默认的 OBS 客户端实例")
    public static void close() {
        if(CLIENT == null) return;
        synchronized (OBSKit.class) {
            if(CLIENT == null) return;
            clientClose(CLIENT);
        }
    }

    @Note("关闭指定的 OBS 客户端实例")
    public static void clientClose(ObsClient client) {
        if(client != null) {
            try {
                client.close();
            } catch (IOException e) {
                throw new IORuntimeException(e);
            }
        }
    }

    /* **************************************************************************************
     *
     *          私有逻辑
     *
     * **************************************************************************************
     * */

    @Note("懒加载工具类默认的 OBS 客户端")
    private static void private_lazyLoadOBSClient() {
        if(CLIENT != null) return;
        synchronized (OBSKit.class) {
            if(CLIENT != null) return;
            OBSSource obsSource = ProfileKit.getObsSource();
            CLIENT = newObsClient(obsSource);
            String border = StrKit.LOG_PRINT_BORDER;
            LOGGER.info("初始化 OBSKit 工具类的 OBS 客户端实例，客户端配置信息如下: \n{}\n{}\n{}", border, obsSource, border);
        }
    }

    @Note("验证必须的字符串不能为空")
    private static void private_stringIsNotNull(String s, String errMsg) {
        if(s == null) throw new NullPointerException(errMsg);
    }

    @Note("验证 OBSBuilder 构建对象是否为 null")
    private static void private_obsBuilderIsNotNull(OBSBuilder builder) {
        if(builder == null) throw new NullPointerException("操作 OBS 的数据项包装实例为 null");
    }

    @Note("OBS 客户端实例的非空校验")
    private static void private_clientNotNull(ObsClient client) {
        if(client == null) throw new NullPointerException("OBS 客户端实例为 null");
    }

    @Note("上传文件到 OBS 对象服务器时添加一些自定义数据和操作")
    private static ObjectMetadata private_additionalHandleAndGetMetaData(OBSBuilder builder) {
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentType(builder.getContentType());
        //是否是保存为额外的存储类型
        StorageClassEnum saveType = builder.getSaveType();
        if(saveType != null) {
            metadata.setObjectStorageClass(saveType);
        }
        //设置属性: 下载文件时指定一个新的文件名称
        String contentDisposition = builder.getContentDisposition();
        if(contentDisposition != null && !contentDisposition.isEmpty()) {
            metadata.setContentDisposition(contentDisposition);
        }
        //是否设置过期时间，单位：天
        int expires = builder.getExpires();
        if(expires > 0) {
            metadata.setExpires(String.valueOf(expires));
        }
        //添加用户自己的数据
        Map<String, Object> userDataMap = builder.getCustomData();
        if(userDataMap != null && !userDataMap.isEmpty()) {
            metadata.setUserMetadata(userDataMap);
        }
        return metadata;
    }


    @Note("上传过程中如果发生异常，那就持续尝试重新上传")
    private static CompleteMultipartUploadResult private_continueRequestUpload(ObsClient client, UploadFileRequest request, int tryCount) {
        if(tryCount == 0) {
            try {
                return client.uploadFile(request);
            } catch (ObsException e) {
                DeleteObjectResult result = client.deleteObject(request.getBucketName(), request.getObjectKey());
                LOGGER.error("上传过程发生异常，当前断点续传次数为 0，已删除已上传的部分，响应结果: {}", result);
                return null;
            }
        }
        if(tryCount > 0) {
            try {
                return client.uploadFile(request);
            } catch (ObsException e) {
                LOGGER.error("上传过程发生异常，当前断点续传次数为 {}，正在执行断点续传", tryCount);
                return private_continueRequestUpload(client, request, --tryCount);
            }
        }
        try {
            return client.uploadFile(request);
        } catch (ObsException e) {
            LOGGER.error("上传过程发生异常，正在执行断点续传，该过程会一直执行，直到上传成功!");
            return private_continueRequestUpload(client, request, tryCount);
        }
    }


    @Note("设置或修改指定对象的属性/元数据")
    private static ObjectMetadata private_setMetaDataHandle(ObsClient client, OBSBuilder builder, Object headers) {
        private_clientNotNull(client);
        private_obsBuilderIsNotNull(builder);
        SetObjectMetadataRequest request = new SetObjectMetadataRequest(builder.getBucketName(), builder.getObjectKey());
        request.setContentType(builder.getContentType());
        Map<String, Object> customData = builder.getCustomData();
        if(customData != null && !customData.isEmpty()) {
            for (Map.Entry<String, Object> e : customData.entrySet()) {
                request.addUserMetadata(e.getKey(), ObjKit.asStringValue(e.getValue(), null));
            }
        }
        StorageClassEnum saveType = builder.getSaveType();
        if(saveType != null) request.setObjectStorageClass(saveType);
        String contentDisposition = builder.getContentDisposition();
        if(contentDisposition != null && !contentDisposition.isEmpty()) {
            request.setContentDisposition(contentDisposition);
        }
        if(headers instanceof Map) {
            Map map = (Map) headers;
            for (Object o : map.entrySet()) {
                Map.Entry e = (Map.Entry) o;
                request.addUserHeaders(ObjKit.asStringValue(e.getKey(), "null"), ObjKit.asStringValue(e.getValue(), "null"));
            }
        }
        else {
            OwnPairs map = (OwnPairs) headers;
            for (Object o : map.entrySet()) {
                Map.Entry e = (Map.Entry) o;
                request.addUserHeaders(ObjKit.asStringValue(e.getKey(), "null"), ObjKit.asStringValue(e.getValue(), "null"));
            }
        }
        return client.setObjectMetadata(request);
    }
}
